Skip to content

feat(cli): add ACP live session lifecycle - #4862

Merged
me2seeks merged 16 commits into
apache:mainfrom
Sun-GLiang:codex/acp-live-session-lifecycle
Sep 13, 2026
Merged

feat(cli): add ACP live session lifecycle#4862
me2seeks merged 16 commits into
apache:mainfrom
Sun-GLiang:codex/acp-live-session-lifecycle

Conversation

@Sun-GLiang

@Sun-GLiang Sun-GLiang commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

ACP clients can prompt, stream, cancel, and close their owned Maka Sessions through Runtime Host. This implements PR4 of the #3132 checklist on top of merged PR3 (#4051).

  • Separate durable Session ownership from a single-flight live attachment. Observe the subscription before starting a Turn, retain it for later prompts, and rely on Host capacity admission. Close releases the subscription and ownership without deleting or archiving the durable Session.
  • Preserve the Session ID after successful Host creation even if optional configuration projection fails. Publish complete configuration updates for attached Sessions, including model-dependent thinking options, with local-set response ordering and close suppression.
  • Support ordered text and local resource links. Validate regular files and size limits, publish links through the existing Session Artifact upload protocol, and pass canonical attachments to Turn admission. Cancel or close during upload aborts staging and prevents Turn start.
  • Reuse the shared Session channel/projector for ordered text/thinking streams, recovery, and terminal detection. Explicit ACP cancellation returns cancelled; authoritative terminal state returns end_turn. Stop uses the exact observed or admitted Turn identity, including a start response that arrives during teardown.
  • Resolve interrupted starts through bounded admission queries without replaying turn.start or treating failed reads as proof of absence. Exhausted queries preserve an outcome_unknown diagnostic. Initial attachment hydration/recovery can be cancelled during close or EOF, and late subscriptions are cleaned up.
  • Fail unsupported interactions belonging to active ACP prompts explicitly and request exact-Turn Stop. Keep idle attachments observing other clients' Turns available for cancellation and close. Keep initial one-shot subscription admission on the reconnecting connection.

Refs #3132

Tool streaming, interaction mapping/answers, and load/resume remain in subsequent checklist PRs. Nonempty client-provided mcpServers remain explicitly unsupported. ACP v1 cannot retract already delivered chunks; non-prefix text/thinking revisions fail with unsupported_stream_revision.

Verification

  • Independently reproduced and fixed all four review findings: local resource-link admission rejection, unknown-admission query failures stranding prompt/cancel/close, EOF deadlock during initial transcript recovery, and loss of idle external Turn cancellation after an interaction.
  • Complete CLI suite: 1004 passed, 3 skipped, including official-SDK/real ACP child-process + Runtime Host resource upload, existing streaming/capacity tests, and lifecycle fault regressions. Query timing is controlled in the stdio recovery matrix.
  • Synchronized current main (41da161d6) and regenerated the combined Windows inventory (91 declarations); validation below covers that merged tree.
  • Reconnecting-connection suite: 25 passed.
  • Full workspace build, typecheck, lint, format, ASF headers, Desktop/UI Knip, Windows test inventory, and git diff --check passed.
  • An independent review of the final fix diff found no additional P0–P2 issues.
  • Full workspace tests and Zed UI smoke were not repeated for this update. Earlier Zed smoke covered create/prompt, streaming, Escape cancellation, and archive-driven close; its session/new payload included mcpServers: [].

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex assisted with implementation, regression tests, independent review, verification, and the PR description. The initial PR also included Codex-assisted real-client validation.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/XXL Over 2500 readable lines label Sep 5, 2026
Preserve committed session reachability, publish authoritative configuration changes through the existing session channel, and harden attachment and close races.

Generated-by: Codex
@Sun-GLiang
Sun-GLiang force-pushed the codex/acp-live-session-lifecycle branch from 9c03a65 to 1ea7f79 Compare September 7, 2026 14:33
@Sun-GLiang
Sun-GLiang marked this pull request as ready for review September 7, 2026 15:47
Cancel authoritative roots on retained attachments even when no ACP prompt is
active. Reject non-regular local resources with a nonblocking open before reads.

ACP v1 cannot retract streamed chunks: reject non-prefix text/thinking revisions
with unsupported_stream_revision, propagate recovery projection errors, and stop
the exact live prompt root. Document this limitation instead of inventing revision
message IDs. Remove unused connection state and duplicate observation handlers.

Cover external roots, FIFO admission, text/thinking clearing, recovery failures,
and subsequent prompts through the existing attachment and mapper seams.

Generated-by: Codex
The POSIX FIFO regression intentionally skips Windows. Regenerate the required skip inventory so the CI inventory check matches the test declarations.

Generated-by: Codex
The capacity scenario shares one harness deadline across 17 creates and 17 turns. Complete independent sessions concurrently before checking retained subscription admission, and give this multi-operation scenario an explicit bounded budget.

A 1-second fixture response delay reproduced the 15-second timeout before the change. With concurrent prompts the same delay and original deadline pass, including four simultaneous repetitions. Capacity rejection, no-turn-on-rejection, and close slot reuse assertions remain intact.

Generated-by: Codex

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for implementing the live ACP lifecycle. I reviewed 8e56e03c with three focused reviewer passes and independent reconciliation. Reusing the Runtime Host channel/projector and keeping execution in the Host is the right direction. Two recoverable P2 cancellation/teardown cases remain inline.

Please keep the fix in the existing prompt/attachment lifecycle: observation ending is not proof that a dispatched Turn was never admitted, and ACP cancellation completion is distinct from successful delivery of Host Stop. No extra cancellation authority or global timeout framework is needed. Repeated exact Stop is already idempotent at the Host; the documented append-only stream-revision limitation is not an additional finding.

Validation: 59 focused checks passed across the targeted runs, plus two independent current-SHA probes reproducing the inline cases. No fresh full CLI build or Zed acceptance run. AI-assisted review; primary-agent reconciliation.

简体中文

感谢实现 ACP 实时生命周期。基于 8e56e03c,三路 reviewer 后由主审独立复核。复用 Host channel/projector、执行归 Host 的方向正确,仍有两项可恢复的 P2,见行内。

修复留在现有 prompt/attachment 生命周期:观察结束不能证明已发送 Turn 未准入;ACP 取消响应与 Host Stop 成功交付也不是同一事实。不需要新增取消权威或全局超时框架。Host 已保证重复 exact Stop 幂等;已声明的 append-only 修订限制不另列问题。

59 项定向检查通过,另有两个当前 SHA 探针复现行内问题。未重新完整构建 CLI 或验收 Zed。AI 辅助评审,主审已复核。

Comment thread packages/cli/src/acp/session-registry.ts
Comment thread packages/cli/src/acp/session-registry.ts Outdated
Keep the Host-returned Turn snapshot until a dispatched start settles so subscription teardown cannot retire exact Stop prematurely. End the cancelled prompt observation when Stop delivery fails, return cancelled in either start ordering, and retain the delivery error on stderr and the teardown result.

Add regressions for late admission during disposal and failed Stop before/after start via both session/cancel and AbortSignal. Verify attachment failure also stops the admitted identity.

Validation: complete CLI suite 899 passed, 3 skipped; build, typecheck, lint, format:check, ASF headers, desktop/UI knip, and diff checks passed.

Generated-by: Codex
Regenerate the Windows test inventory from the merged sources, retaining the ACP FIFO exclusion and current main entries.

Generated-by: Codex
Wait for pending configuration setters before delivering refreshes from a first or replacement attachment. Cover both races so a delayed setter response cannot overwrite newer configuration notifications.

Generated-by: Codex

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fixes. I rechecked 1ef5b621 and confirmed that both previous P2s are resolved. I found one adjacent cancellation gap and have left it inline. It also reproduces on 8e56e03c; I missed it in the earlier review, so this is not a regression introduced by the latest fixes.

AI-assisted review: 66 focused checks passed, and seven regression checks failed against the previous registry implementation. The additional registry probe below fails on both heads. I did not repeat the Zed smoke test.

简体中文

感谢修复。我复核了 1ef5b621,之前两项 P2 均已解决。还有一个相邻的取消问题,已写在行内评论。它在 8e56e03c 上也能复现,是我上轮遗漏的边界,并非本轮修复引入的回归。

本次为 AI 辅助评审:66 项针对性检查通过,7 项回归检查在旧 registry 实现上失败;新增的 registry 探针在新旧两个 head 都失败。本轮未重跑 Zed。

Comment thread packages/cli/src/acp/session-registry.ts Outdated
Keep the original prompt attempt until Host subscription or turn.query facts resolve a dispatched start whose response was lost. Stop the recovered exact Turn, or retire the attempt on authoritative not_found or terminal state, without replaying start.

Cover cancellation before and after interruption, subscription recovery after a query timeout, authoritative query outcomes, and shutdown after observation closes.

Generated-by: Codex

@me2seeks me2seeks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review (Command Code) — not an approval

The lifecycle design is sound and the ACP surface correctly reuses the existing channel/projector rather than forking a parallel path. One defect contradicts the PR's own recovery claim, and one leaves a reachable class of turns unable to complete. Line numbers are the head revision.

P1 (Must-Fix) — the connection wrapper drops reconnecting, so every recoverable Host error fails the attachment instead of resyncing.

runMakaAcpStdioServer validates the connection and then hands downstream a new object literal:

// packages/cli/src/acp/stdio-server.ts:56-65
if (!isRuntimeHostReconnectingConnection(connection)) { ... throw ... }
return {
  request: connection.request.bind(connection) as RuntimeHostConnection['request'],
  openSessionSubscription: connection.openSessionSubscription.bind(connection),
  openSessionSubscriptionOnce: connection.openSessionSubscriptionOnce.bind(connection),
  close: () => context.close(),
};

That literal omits reconnecting. The type it satisfies (AcpSessionRegistryConnection, packages/cli/src/acp/session-registry.ts:92-96) Picks only 'request' | 'openSessionSubscription' | 'openSessionSubscriptionOnce' | 'close', so the omission type-checks. The registry passes that same object to the channel (openRuntimeHostSessionAttachment, session-registry.ts:897-898), and the channel's recovery gate is exactly that marker:

// packages/cli/src/runtime-host-session-channel.ts:590-591
#canRecover(error: unknown): boolean {
  if (!isRuntimeHostReconnectingConnection(this.#connection)) return false;
// packages/runtime-host/src/client/reconnecting-connection.ts:112-116
export function isRuntimeHostReconnectingConnection(connection: unknown): connection is ... {
  return (connection as Partial<...>).reconnecting === true;
}

this.#connection is that literal (runtime-host-session-channel.ts:98, 137), so #canRecover is always false on the ACP path. Every recoverable condition (connection_closed, sequence_gap, projection_revision_invalid, transcript_release_failed, slow_consumer) is routed to #fail(error) instead of #recover(...), which fires onFailed#retireFailedAttachmentfailTurn on the active prompt. A Host restart or a slow_consumer eviction therefore fails the live session/prompt with an internal error instead of resyncing — the opposite of the stated "recovery uses the existing reconnecting path". The wrapper even binds openSessionSubscriptionOnce correctly, so recovery would work; only the gate is defeated.

Why CI cannot see it: the only other channel owner (the TUI driver) passes the real impl instance, where reconnecting = true is an own property. In this package, reconnecting appears outside tests only on the impl class — the ACP tests inject it on the mocked context.connection (packages/cli/src/__tests__/acp-stdio-server.test.ts:311), i.e. before production re-wraps it, and the registry's own fake omits it entirely. No test drives session/prompt through the real stdio wrapper.

Smallest sound fix: widen the Pick to include 'reconnecting' and set reconnecting: true on the returned literal (two lines). A test that runs a prompt through the real wrapper would also close the blind spot.

P2 (Should-Fix) — interaction requests are discarded, and no ACP path can answer one, so an affected turn cannot complete.

The attachment wires the interaction callbacks to no-ops:

// packages/cli/src/acp/session-registry.ts:906-907
onInteractionPending: () => undefined,
onInteractionResolved: () => undefined,

and the event mapper handles only text_delta | text_complete | thinking_delta | thinking_complete | complete | error | abort, with default: break (packages/cli/src/acp/session-event-mapper.ts:85). So user_question_request, form_request and sandbox_boundary_request are dropped, and there is no session/request_permission or interaction-answer call anywhere under packages/cli/src/acp/. The turn has a permission_mode config option, so a client can select ask; a turn that then raises an interaction has no path to a settlement. Deferring interactions to a follow-up is a legitimate scope decision — leaving the affected prompt with no client-visible signal is not. Suggest turning onInteractionPending into an explicit turn failure with a clear unsupported_interaction code (the PR already uses that fail-with-diagnostic shape when Stop delivery fails), so the client sees an error instead of nothing.

Note this is permission-adjacent surface, but the diff does not bypass any permission or sandbox decision — the defect is fail-to-respond, not fail-open.

Review-relevant risks. The P2 surface touches permissions, so an independent human should look at it under CONTRIBUTING.md. No credential, release, licensing or governance effect was identified.

Required conclusion.

  1. Optimal for the actual problem? Largely yes for the prompt/stream/cancel/close core; not optimal given P1 — one missing marker disables the capability the design documents.
  2. Production code that can be deleted? none identified.
  3. Low-quality tests to delete or replace? none identified. The gap is coverage, not quality: the production connect() wrapper, a recoverable subscription error on an ACP attachment, and any interaction becoming pending are all untested.
  4. Deeper refactor required? No. Two targeted changes: propagate the marker, and stop swallowing interactions.
  5. Ready to merge? Not as-is. P1 is a one-line fix that contradicts a central claim of the PR; P2 needs at least an explicit failure path.
  6. Residual risks / verification gaps: I did not run the suite (the PR's pass counts are unverified). A cancelled turn can keep emitting agent_message_chunk after the prompt already returned cancelled — off-spec once the response is sent, unverified against a real client.

Approval boundary. This is automated review; it is not an approval. Per CONTRIBUTING.md, the merge decision requires an independent human review. No approve was submitted.

Retain the reconnecting marker across the stdio connection wrapper so the shared channel can recover. Fail pending unsupported interactions through the existing exact-Turn Stop path, with a protocol diagnostic.

Generated-by: Codex
@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

Automated follow-up (Codex), responding to review 5170016599. This is not an approval.

Checked the findings against the current #3132 implementation checklist and reproduced both defects at the reviewed head. Fixed in ccaba9876.

  • P1 accepted. PR4 explicitly owns active-stream recovery. The stdio wrapper now preserves reconnecting: true, and AcpSessionRegistryConnection requires the marker. A regression drives session/new and session/prompt through the production stdio wrapper, registry, and channel; a controlled slow_consumer frame must reopen the subscription and finish with end_turn, without stopping the Turn. It failed before the fix and passes afterward. Initial attachment still uses the one-shot opener.
  • P2 accepted as a minimal lifecycle safeguard, with the PR5 scope preserved. The checklist assigns full tool/interaction mapping, answers, and unsupported-client handling to PR5. This change only prevents PR4's exposed prompt route from silently hanging: a pending interaction retires observation, returns JSON-RPC -32603 with error.data.code: unsupported_interaction and the interaction kind, and reuses the existing failure path to request Stop for that prompt's exact Host identity. It neither answers nor approves an interaction and does not invent Host terminal state. Production-stdio-route tests cover all five kinds and assert the exact Stop input. Full permission/elicitation protocols remain in PR5. The adapter README documents this temporary behavior.
  • Pushback on the residual cancellation claim as an established defect. AcpSessionEventMapper.cancel() is serialized with notification delivery and latches the terminal result; accept() checks that terminal before emitting further chunks. Added a regression injecting late text/thinking deltas and completions after cancellation completes: no additional notification is emitted. Please provide a concrete production-route reproduction if another path can emit after the prompt response. This test does not claim to replace a fresh real-client UI validation.

Validation: full CLI suite 974 passed, 3 skipped, including the existing official-SDK child-process/real-Host tests. Full workspace build, typecheck, lint, format, ASF headers, Desktop/UI Knip, and git diff --check passed. New recovery/interaction fault injection uses controlled Host subscriptions through the real stdio wrapper; it is not a native Host-restart or Zed test. The full workspace test suite and Zed smoke were not repeated for this follow-up.

@me2seeks me2seeks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up (Command Code) — verified resolved

I re-checked the head after your push.

The P1 is fixed. The connection wrapper returned from the stdio server now carries reconnecting: true alongside the bound methods, so isRuntimeHostReconnectingConnection returns true and the channel's recovery gate can engage — the ACP path now gets the resync behaviour the design describes instead of failing the attachment.

The P2 is addressed too: the interaction callbacks are no longer no-ops, so a pending interaction is no longer silently discarded with the prompt left hanging.

Thanks for turning these around. (Automated review; not an approval.)

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed ccaba9876. Architecture: the seam is right. Durable ownership (#ownedSessionIds) is separate from the single-flight attachment (#attachments), the first prompt attaches and starts eventsForTurn before turn.start, capacity stays with Host admission, and the registry consumes RuntimeHostSessionChannel through the same open/eventsForTurn/failTurn/snapshot surface the TUI driver uses. Nothing re-derives Turn state: cancellation and Stop always take identity from the subscription root, the turn.start result or turn.query. All five prior findings are fixed and their regressions go red when the fixes are reverted.

Not approvable yet: two P2s remain, both local to session-registry.ts, both on ② recovery paths. Nothing at P0/P1.

Prior finding Fix Status
P2 :810 dispose retired cancellation before dispatched start resolved 3275e4aba Resolved; startedTurn retained, #stopPromptWhenObservable loops until finished.
P2 :409 prompt pending after failed Stop 3275e4aba Resolved; #cancelPrompt fails only this prompt's observation.
P2 :451 unknown admission retired cancellation c17ca7f2a Resolved for the transient branch (10 tests go red on revert). The permanent branch is P2 #1 below.
me2seeks P1 wrapper dropped reconnecting ccaba9876 Resolved; AcpSessionRegistryConnection requires the marker, 1 test red on revert.
me2seeks P2 interactions no-op'd ccaba9876 Resolved; 5 scenarios red on revert, exact-Turn Stop asserted.

P2 — Unknown admission never settles when recovery is permanently dead; prompt, cancel, close and dispose wedge

Reachability ②: turn.start dispatched, response lost, then reconnect fails permanently (RuntimeHostPermanentReconnectError) or the Host never returns before client EOF.

session-registry.ts:352-362 settles unknown admission only on not_found; onFailed#retireFailedAttachment (:536-542, :570-587) fails the queue and wakes the prompt but leaves admissionSettled=false, so #stopPromptWhenObservable (:476-477) waits again with no fact source left. #cancelPrompt awaits that task, and #dispose (:811-813) awaits cancellations before #closeOwnedConnection, so the query can only be rejected by the close that dispose never reaches.

Repro (registry fixture): prompt → reject start with RuntimeHostRequestInterruptedError('turn.start','command','dispatched','connection_lost') → reject turn.query with a non-not_found error → attachment.failAttachment(...). Prompt, cancel and dispose() stay pending; Stop count 0.

Fix inside the existing attempt: in #retireFailedAttachment set admissionSettled = true for matching prompts before #wake; likewise settle when turn.query rejects with anything the reconnecting connection will not retry, keeping only connection_lost/timeout as "still unknown". Add the permanent case to the retains cancellation … unknown admission matrix. Reordering #dispose also unwedges shutdown, but settling the attempt is the root fix.

P2 — Configuration refresh failure during a live prompt stops the Host Turn

Reachability ②, triggered from ①: the Host bumps the session header during a normal first Turn (execution-composition.ts:1014-1016 nameSessionIfUnnamed), so metadataRevision changes while streaming and onSnapshotChanged (session-registry.ts:509-525) reads both catalogs. Any failure there is routed to #retireFailedAttachment (:521-525), which failTurns the active prompt; #prompt's catch then sends turn.stop for the running Turn (:379-381) and the client gets an unmapped Internal error.

Repro (registry fixture): prompt with root running → make session.catalog.query throw once → attachment.setMetadataRevision(2). Prompt rejects with the raw error, turn.stop called once, attachment closed.

This contradicts #create's own stance (:654-662: projection failure must not fail a committed Host result) and the checklist, which makes config_option_update a presentation notification. Fix: make the refresh best-effort — log and keep the attachment and prompt; the next metadataRevision change retries. That also deletes the earlyFailure branch on this path. Keep retirement for genuine channel failures (onFailed).

Complexity

openSessionSubscriptionOnce (+22 lines across four files) differs from openSessionSubscription only in fail-fast vs wait-for-reconnect on the first attach, since the lifecycle already rejects root/protocol/composition changes permanently (reconnecting-connection.ts:78-102). It is what the #3132 checklist asks for; noted so the cost is explicit. session-event-mapper.ts:80 re-derives terminal classification the channel already owns (inline). The tail/retired/delivery serialization is the minimum for the ordering the checklist demands.

Tests: node --test --test-concurrency=4 "dist/__tests__/acp-*.test.js" 102 pass (incl. 10 real Runtime Host child-process tests); reconnecting-connection.test.js 25 pass. Not repeated: full CLI suite, Zed smoke.

中文

复核 ccaba9876。架构:接缝正确。持久 Session 所有权(#ownedSessionIds)与单飞 attachment(#attachments)分离,首个 prompt 先 attach、先启动 eventsForTurnturn.start,容量归 Host 准入;registry 通过与 TUI driver 相同的 open/eventsForTurn/failTurn/snapshot 面消费 RuntimeHostSessionChannel。没有重新推导 Turn 状态:取消和 Stop 的身份始终来自订阅 root、turn.start 结果或 turn.query。五项既往意见均已修复,撤销修复后对应回归全部变红。

尚不能 approve:还剩两项 P2,都局限在 session-registry.ts,都在 ② 恢复路径上。无 P0/P1。(状态表见上)

P2 — 恢复永久失败时未知准入永不收敛;prompt、cancel、close、dispose 全部卡死。 可达 ②:turn.start 已派发、回执丢失,随后重连永久失败(RuntimeHostPermanentReconnectError)或 Host 在客户端 EOF 前一直不回。:352-362 只在 not_found 时收敛;onFailed#retireFailedAttachment:536-542:570-587)只 fail 队列并唤醒,admissionSettled 仍为 false,#stopPromptWhenObservable:476-477)再次等待却已无事实来源。#cancelPrompt 等这个任务;#dispose:811-813)先等取消再关连接,query 只能被 dispose 永远到不了的那次关连接拒绝。复现:prompt → start 以 dispatched/connection_lost 拒绝 → turn.query 以非 not_found 错误拒绝 → attachment.failAttachment(...);prompt、cancel、dispose() 永久 pending,Stop 为 0。修法在现有 attempt 内:#retireFailedAttachment 对匹配 prompt 置 admissionSettled = true 再唤醒;turn.query 遇到重连连接不会重试的错误同样收敛,只把 connection_lost/timeout 视为仍未知;在 retains cancellation … unknown admission 矩阵补永久失败一例。改 #dispose 顺序也能解开关机,但收敛 attempt 才是根治。

P2 — 直播 prompt 期间配置刷新失败会 Stop 掉 Host Turn。 可达 ②,由 ① 触发:正常首轮 Host 会写 session header(execution-composition.ts:1014-1016 自动命名),metadataRevision 在流式期间变化,onSnapshotChanged:509-525)去读两个 catalog。任一失败都走 #retireFailedAttachment:521-525)→ failTurn 活跃 prompt → #prompt 的 catch 对运行中的 Turn 发 turn.stop:379-381),客户端只收到未映射的 Internal error。复现:root running 时让 session.catalog.query 抛一次 → setMetadataRevision(2);prompt 以原始错误拒绝、turn.stop 一次、attachment 关闭。这与 #create:654-662)自己的立场和 checklist(config_option_update 是展示层通知)矛盾。修法:刷新改为尽力而为——记日志,保留 attachment 和 prompt,下次 metadataRevision 变化重试;同时可删该路径的 earlyFailure 分支。真正的 channel 失败(onFailed)仍保留退役。

复杂度。 openSessionSubscriptionOnce(四个文件共 +22 行)与 openSessionSubscription 的唯一差异是首次 attach 快速失败 vs 等待重连,生命周期已对 root/协议/composition 变化永久拒绝(reconnecting-connection.ts:78-102);这是 #3132 checklist 的要求,只为把代价写明。session-event-mapper.ts:80 重新推导 channel 已负责的终态(行内)。tail/retired/delivery 串行化是满足 checklist 排序的最小实现。

测试:node --test --test-concurrency=4 "dist/__tests__/acp-*.test.js" 102 通过(含 10 个真实 Runtime Host 子进程测试);reconnecting-connection.test.js 25 通过。未重跑:完整 CLI 套件、Zed 冒烟。

Comment thread packages/cli/src/acp/session-event-mapper.ts Outdated
Comment thread packages/cli/src/acp/session-registry.ts
Comment thread packages/cli/src/acp/session-registry.ts Outdated
Comment thread packages/cli/src/acp/session-registry.ts
Converge permanently failed and shutdown-only unknown admissions while preserving exact Stop identity from a late turn.start response. Keep configuration projection best-effort, map observation failures at the ACP boundary, and decouple prompt streaming from catalog reads.

Retain recoverable Session errors as nonterminal because Runtime may emit them before the authoritative completion.

Generated-by: Codex
@Sun-GLiang
Sun-GLiang force-pushed the codex/acp-live-session-lifecycle branch from f5b3226 to ecfe143 Compare September 11, 2026 14:57
@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

Reworked the review findings against the current head, ecfe143f8, after resetting the previous follow-up to ccaba9876.

  • P2 unknown admission — accepted. Permanent turn.query and attachment failures now settle the local attempt. A still-pending turn.start remains protected because its late response may carry exact Stop identity. During EOF, the owned connection is closed only after turn.start settles and only when admission remains query-only unknown. Regressions cover prompt, cancel, close, dispose, late started/interrupted responses, and a query that never settles.
  • P2 configuration refresh — accepted. Failed or stalled catalog projection no longer retires the attachment, stops the Host Turn, delays streamed chunks, or delays the terminal prompt response. A later metadata revision retries projection, and close still suppresses queued notifications.
  • Observation error mapping — accepted. Exhausted subscription recovery now returns stable ACP error data while preserving existing ACP RequestError values.
  • Recoverable-error terminal mapping — pushback. Runtime can emit error { recoverable: true } before the authoritative complete; converting every error directly to end_turn would terminate ACP early.
  • Close-first disposal — adjusted rather than applied literally. Exact Stop identity remains preferred; connection close is the bounded fallback only after the start response can no longer provide that identity.

Validation on ecfe143f8: complete CLI suite 990 passed, 3 skipped; full build, typecheck, lint, format, ASF headers, Desktop/UI Knip, and git diff --check passed. Full workspace tests and Zed smoke were not repeated. AI-assisted implementation and response: Codex.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed ecfe143f8. Both P2s are fixed inside the existing prompt attempt and every new regression goes red with its production change reverted; my inline on session-event-mapper.ts:80 was wrong and is withdrawn (the channel finishes a Turn queue only on Host terminal facts, and the kernel treats error as non-terminal). Not approving yet, for a reason I own part of: the registry now carries about 1.1k production and 2.7k test lines for prompt/stream/cancel/close, and a large share of that is a state machine the channel already provides — including the unknown-admission latch my earlier rounds asked to harden rather than remove.

Prior finding Status
P2 unknown admission never settles on permanent failure Fixed. #retireFailedAttachment and non-retryable turn.query rejections settle the attempt; original repro now settles prompt/cancel/dispose with 0 Stops. 7 tests red on revert.
P2 configuration refresh failure stops the Host Turn Fixed; best-effort, next metadataRevision retries. 1 red on revert.
P3 mapper recoverable check Declined; author right. Withdrawn.
P3 observation-path errors unmapped Fixed (subscription_failure + reason). 2 red.
P3 chunks gated on configuration.tail Fixed. 2 red.
P3 #dispose order Adjusted: close early only after every dispatched turn.start settled. 3 red.

P2 — The unknown-admission latch duplicates what the channel snapshot already gives

Reachability ② (lost turn.start response). ActiveAcpPrompt.{admissionSettled, startRequestSettled, startedTurn, waiters}, the turn.query fallback (session-registry.ts:349), the while (!finished) loop in #stopPromptWhenObservable (:455-481), #waitForPromptChange (:637), the latching in #retireFailedAttachment and the unknownAdmissions block in #dispose exist so a prompt whose start response was lost stays open until a later cancel can Stop the exact Turn. The TUI driver on the same channel does not have this: it failTurns and throws on a start failure (runtime-host-session-driver.ts:385-388) and takes Stop identity from channel.snapshot.rootTurn at stop time (:1084-1099). turn.start is never replayed by the reconnecting connection, so the latch only buys waiting for recovery to reveal the root, and the registry already stops an observed non-terminal root on an idle attachment in #cancelSession (:414-437); close cancels first. Replace the latch with: fail the prompt with request_interrupted; if cancelled and turn.start resolved started, Stop result.turn; otherwise Stop snapshot.rootTurn when it matches. The checklist's "deterministic termination on subscription failure" and "cancel via snapshot identity" are both met by that. About −120 production, −400 test (acp-session-registry.test.ts:584-981). I asked for the opposite in the :451 round; the driver comparison is what changes my view.

P2 — Registry tests run against a hand-written channel, not the channel

AcpSessionAttachment / openSessionAttachment (session-registry.ts:99, :123, :179) have one production implementation and exist so acp-session-registry.test.ts can inject FakeAcpSessionAttachment (:2933, ~270 lines); the test's fakeConnection throws on both subscription opens, so ~1000 lifecycle test lines never touch RuntimeHostSessionChannel. Those tests re-specify when rootTurn appears and when queues finish instead of asserting the channel's behaviour, which is how the unknown-admission and configuration-ordering mechanisms grew without a production counterexample. The stdio-server and driver tests already drive the real channel through FakeSubscription; the registry tests can use the same harness. Delete the two interfaces, the option and the fakes; keep the real-connection tests.

P3 — Smaller cuts

  • Configuration promise queue (tail/retired/delivery/#pendingConfigSets, :130-136, :165-228, :491-500): metadataRevision and the CAS revision are the same header revision, so a last-published-revision compare orders sets against refreshes with no queue; retired is never awaited. About −65 / −290.
  • openSessionSubscriptionOnce (reconnecting-connection.ts:49, :219; channel :72, :169; stdio-server.ts:64; registry :929): it pins the epoch only at call time; the following turn.start and every later channel #recover use whatever epoch is current, exactly as the retrying open does. The checklist line it implements ("attachment never silently lands on a replacement Host epoch") is not a guarantee this code provides, and the TUI does not have it. Open the channel like the driver and drop the extra path (~70 lines).
  • resource_link admission (prompt-content.ts, ~120 of 149 lines + 174 test): the checklist asks for "supported ACP prompt content"; is there evidence a real client (the Zed smoke) sends resource_link? If not, text-only meets PR4 and attachments return with a client that uses them.
  • packages/cli/src/acp/README.md restates error codes and behaviour that live in the registry; every later checklist PR will change it. Drop.

A PR4 with those cuts is roughly 450 production and 700 test lines for the same checklist.

Tests on this head: node --test --test-concurrency=4 "dist/__tests__/acp-*.test.js" 127 pass. Not repeated: full CLI suite, Zed smoke.

中文

复核 ecfe143f8。两项 P2 都在现有 prompt attempt 内修好,所有新回归在撤销对应生产改动后变红;我在 session-event-mapper.ts:80 的行内意见是错的,撤回(channel 只在 Host 终态事实上结束 Turn 队列,kernel 把 error 视为非终态)。暂不 approve,原因我自己也有份:registry 现在为 prompt/stream/cancel/close 背着约 1.1k 生产和 2.7k 测试行,其中很大一部分是 channel 已经提供的状态机——包括我前几轮要求加固而不是删除的未知准入闩锁。

既往意见 状态
P2 永久失败时未知准入永不收敛 已修。#retireFailedAttachment 和不可重试的 turn.query 拒绝都收敛 attempt;原复现现在 prompt/cancel/dispose 全部收敛、Stop 为 0。撤销后 7 个测试变红。
P2 配置刷新失败 Stop 掉 Host Turn 已修;尽力而为,下次 metadataRevision 重试。撤销后 1 红。
P3 mapper recoverable 判断 拒绝;作者对。撤回。
P3 观察路径错误未映射 已修(subscription_failure + reason)。2 红。
P3 chunk 等 configuration.tail 已修。2 红。
P3 #dispose 顺序 已调整:只在所有已派发 turn.start 收敛后才提前关连接。3 红。

P2 — 未知准入闩锁重复了 channel snapshot 已提供的东西

可达 ②(turn.start 回执丢失)。ActiveAcpPrompt.{admissionSettled, startRequestSettled, startedTurn, waiters}turn.query 回退(session-registry.ts:349)、#stopPromptWhenObservablewhile (!finished) 循环(:455-481)、#waitForPromptChange:637)、#retireFailedAttachment 的闩锁和 #disposeunknownAdmissions 块,都是为了让 start 回执丢失的 prompt 保持打开,直到后续 cancel 能 Stop 精确的 Turn。同一 channel 上的 TUI driver 没有这些:start 失败就 failTurn 并抛出(runtime-host-session-driver.ts:385-388),Stop 身份在停止时取 channel.snapshot.rootTurn:1084-1099)。重连连接从不重放 turn.start,闩锁只买到"等恢复揭示 root",而 registry 已经在 #cancelSession:414-437)里对闲置 attachment 上观察到的非终态 root 发 Stop;close 先取消。用这套替换闩锁:prompt 以 request_interrupted 失败;若 cancelledturn.start 返回 started,Stop result.turn;否则匹配时 Stop snapshot.rootTurn。checklist 的"订阅失败时确定性终止"和"用 snapshot 身份取消"都由此满足。约 −120 生产、−400 测试(acp-session-registry.test.ts:584-981)。我在 :451 那轮要求的是相反方向;改变看法的是与 driver 的对比。

P2 — registry 测试跑在手写的 channel 上,不是 channel 本身

AcpSessionAttachment / openSessionAttachmentsession-registry.ts:99:123:179)只有一个生产实现,存在只是为了让 acp-session-registry.test.ts 注入 FakeAcpSessionAttachment:2933,约 270 行);测试的 fakeConnection 对两个订阅 open 都抛异常,约 1000 行生命周期测试从不接触 RuntimeHostSessionChannel。这些测试重新规定 rootTurn 何时出现、队列何时结束,而不是断言 channel 的实际行为——未知准入和配置排序机制就是这样在没有生产反例的情况下长出来的。stdio-server 和 driver 的测试已经用 FakeSubscription 驱动真实 channel;registry 测试可以用同一套。删掉两个接口、选项和假类;保留真连接测试。

P3 — 较小的删减

  • 配置 promise 队列tail/retired/delivery/#pendingConfigSets:130-136:165-228:491-500):metadataRevision 与 CAS revision 是同一个 header 修订号,比较最近发布的修订号即可把 set 与刷新排序,不需要队列;retired 从未被等待。约 −65 / −290。
  • openSessionSubscriptionOncereconnecting-connection.ts:49:219;channel :72:169stdio-server.ts:64;registry :929):它只在调用瞬间固定 epoch;之后的 turn.start 和每次 channel #recover 都用当前 epoch,与重试式 open 完全一样。它实现的 checklist 那句("attachment 绝不悄悄落到替换 Host epoch")不是这段代码提供的保证,TUI 也没有。像 driver 一样打开 channel,去掉这条额外路径(约 70 行)。
  • resource_link 准入prompt-content.ts,149 行中约 120 行 + 174 行测试):checklist 要求"支持的 ACP prompt 内容";有没有证据真实客户端(Zed 冒烟)发过 resource_link?没有的话纯文本即满足 PR4,附件等有客户端用它时再回来。
  • packages/cli/src/acp/README.md 复述 registry 里的错误码和行为;后续每个 checklist PR 都会改它。删。

做完这些删减的 PR4 大约是 450 生产 + 700 测试行,覆盖同一份 checklist。

本 head 测试:node --test --test-concurrency=4 "dist/__tests__/acp-*.test.js" 127 通过。未重跑:完整 CLI 套件、Zed 冒烟。

Comment thread packages/cli/src/acp/session-event-mapper.ts Outdated
Comment thread packages/cli/src/acp/session-registry.ts Outdated
Let the Runtime Host Session channel own terminal detection while cancellation drains already accepted notifications. Remove the test-only attachment abstraction and dead retirement state, then exercise the registry through real channels and subscriptions.

Ignore presentation-refresh failures after their attachment is no longer live, while retaining diagnostics for active refreshes.

Generated-by: Codex
@Sun-GLiang

Sun-GLiang commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

AI-assisted follow-up; not an approval. Addressed review 5181707546 in 0ba096733.

Accepted and fixed:

  • The Runtime Host Session channel is now the sole terminal authority. The mapper only serializes projection delivery; the registry iterates the Turn queue to its authoritative end. Cancellation still drains notifications already accepted before returning cancelled.
  • Removed the unread retired state.
  • Removed AcpSessionAttachment, its injectable opener, and the hand-written attachment/event-stream fakes. Registry lifecycle coverage now uses the production RuntimeHostSessionChannel with FakeSubscription: 19 real-channel instances cover concurrent/opening attachment, cancel/close/dispose, exact Stop identity, late and unknown admission, Stop failure, failed-channel replacement, external roots, configuration failure/stall/close races, and observation failure.
  • Queue-end timing exposed an in-flight presentation refresh being interrupted after close. Diagnostics are now emitted only while that exact attachment configuration is still live; active refresh failures remain visible.

Pushback on the remaining cuts:

  • Unknown-admission latch: the snapshot is not sufficient in the reachable interval where turn.start was dispatched, its response was lost, and cancel/close observes rootTurn = null before recovery. If the prompt attempt is retired then, the later recovered running root has no retained cancellation owner and receives no Stop. This is the same race previously reproduced in discussion_r3969694107. The new production stdio/channel regression waits until turn.query is pending, recovers the root, and proves exactly one Stop before returning cancelled. The TUI comparison does not provide ACP prompt/cancel response semantics, so I retained the minimum attempt state.
  • Configuration queue: equal header revision domains do not serialize asynchronous projection. A local r2 CAS can commit and stall in connection-catalog projection while an external r3 snapshot arrives. A last-published-revision check alone either exposes r3 before the r2 response or drops the r2 notification while returning r2, violating the defined set-response ordering. The new real stdio/channel test gates that exact r2 → r3 race and asserts notifications bypass then ask, with the set response still bypass. delivery is also the narrow promise close must await; the dead retired field was removed independently.
  • openSessionSubscriptionOnce: the tracker explicitly requires first attachment not to wait through and silently rebind to a replacement Host. The one-shot path uses the currently available concrete connection and fails on loss; the regular opener waits/retries and is reserved for channel recovery. The existing reconnecting-connection regression asserts that the replacement receives zero initial-open calls. Later Turn start/recovery using the current compatible epoch does not erase this initial-attachment boundary.
  • resource_link: the vendored official @agentclientprotocol/sdk@1.4.0 schema states that baseline agents MUST support both ContentBlock::Text and ContentBlock::ResourceLink (schema.json, PromptCapabilities and PromptRequest.prompt). Real-client frequency cannot narrow that protocol requirement, so the implementation and tests remain.
  • ACP README: the Proposal: Agent Client Protocol (ACP) interface for Maka (maka --acp) #3132 tracker requires documentation/capability updates with each feature. This short file records the temporary PR4 lifecycle, stream-revision, and unsupported-interaction contract that public method types do not express; it should evolve with later PRs rather than be deleted now.

Validation: ACP tests 96/96; complete CLI suite 959 passed, 3 skipped; full workspace build, typecheck, lint, format check, ASF headers, Windows test inventory, Desktop/UI Knip, and git diff --check passed. The local npm was 11.17.0 rather than the repository-requested 11.19.0; required CI will validate the exact toolchain. Full non-CLI workspace tests and a fresh Zed smoke were not repeated. AI-assisted implementation and response.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reworking the tests through the real Session channel and for the concrete counterexamples to the earlier deletion requests. I re-reviewed 0ba09673387cbde651b9474fdd5a313cc6b5f6a0 with separate architecture/ablation and implementation passes. One P2 cancellation edge remains, noted inline.

I am withdrawing the earlier broad recommendation to remove the unknown-admission latch and configuration queue. The ACP request/response obligations are not equivalent to the TUI driver's: cancellation can need an identity that appears only after recovery, and asynchronous r2 projection must retain its notification/response order when an external r3 update arrives. These are request/delivery coordination concerns, while the channel/projector remains the sole authority for recovery, sequence, and Turn termination.

The other retained pieces also have concrete contracts: the tracker requires one-shot initial attachment at the reconnecting boundary; the SDK baseline requires ResourceLink support; the tracker requires incremental capability documentation. The fake attachment/opener and unread retired state have been removed. I would not repeat those deletion requests or split this connected lifecycle solely because the diff is XXL.

There is one small optional cut: AcpPromptContentDependencies.openFile exists only for the oversized-file test. A sparse temporary file larger than the limit could exercise the real filesystem seam and remove that test-only dependency surface. This is a small maintenance improvement, not a blocker or a reason for another abstraction.

The remaining inline issue is narrower than the prior unknown-admission fix: a failed recovery query is still being treated as permission to release the cancellation owner before the channel can recover the running Turn. Please preserve deterministic settlement on a genuinely failed attachment while fixing this case through the existing channel lifecycle.

Validation: an exact-snapshot build and a focused production stdio/RuntimeHostSessionChannel probe reproduced a dispatched start losing its reply, followed by turn.query returning internal_failure: the prompt returned cancelled with zero Stops, and later subscription recovery still produced zero Stops for the running root. Current GitHub checks are successful/skipped. The complete suite and Zed UI smoke were not repeated in this review.

AI assistance: Codex coordinated two deep reviewers and cross-checked the production path and focused reproduction.

Comment thread packages/cli/src/acp/session-registry.ts Outdated
A failed turn.query does not establish whether a dispatched start was
admitted. Keep its cancellation attempt until a query or channel recovery
provides the exact Turn to stop, while preserving final attachment failure
and shutdown cleanup.

Reuse the admission query after recovery when the snapshot lacks the
matching root: that snapshot may have been taken before start admission.
Only Host not_found establishes absence. Extend the production stdio and
real Session channel regression across query errors, delayed hydration,
recovered terminal/absent/unrelated roots, and final attachment failure.

Generated-by: Codex
Publish local resource links as canonical Session Artifacts before Turn admission,
abort staged uploads on cancellation, and retain input ordering and file checks.

Bound unknown-admission queries without treating read or observation failures as
proof of absence. Keep exact Stop identity across teardown, cancel pending initial
hydration and recovery, and preserve idle external roots with pending interactions.

Cover all four reported failures with production-route and real Host regressions.

Generated-by: Codex
Merge main at 41da161 so the ACP lifecycle fixes are verified against the
current Host compatibility epoch. Regenerate the combined Windows test
inventory totals used by CI.

Generated-by: Codex
@Sun-GLiang

Copy link
Copy Markdown
Contributor Author

Addressed the remaining P2 on the current head, 87b1572b8 (implemented in 7cf953e75 and hardened in bb67cefc8).

  • A failed turn.query, including internal_failure, no longer sets admissionSettled or releases the cancellation owner.
  • The pending cancellation remains attached to the existing channel lifecycle. A matching recovered root provides the exact Stop identity; a terminal root or authoritative not_found settles without sending Stop.
  • Subscription failure does not imply that the dispatched start was absent. Admission queries are bounded by per-attempt deadlines and a finite retry count; exhaustion returns a stable outcome_unknown failure instead of waiting forever.
  • The production stdio / real RuntimeHostSessionChannel regression now covers internal_failure before running recovery, an empty snapshot with delayed hydration, terminal/absent/unrelated roots, pending queries, and final attachment failure.

Focused validation on the current head: CLI build passed; all 9 outcome-unknown ACP cancellation cases passed; git diff --check passed. Current GitHub checks are successful/skipped.

I left the optional AcpPromptContentDependencies.openFile cleanup out of this follow-up because it is unrelated to the cancellation correctness issue and was explicitly non-blocking.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the recovery boundary and adding production-path regressions. I re-reviewed 87b1572 against the PR4 scope in #3132, including the changes since 0ba0967. No P0–P2 findings remain; I resolved the two outstanding unknown-admission threads.

A failed recovery query no longer means admission was absent. The existing prompt attempt retains cancellation while subscription/query evidence can provide the exact Turn identity. Query exhaustion has an explicit outcome_unknown failure boundary, rather than silently claiming the Host stopped or waiting forever. Initial attachment hydration can now be cancelled at EOF, with late subscriptions cleaned up; an idle attachment also retains external-Turn identity for cancel/close.

The resource-link path now uploads through the existing Host Artifact protocol before Turn admission and uses the canonical returned attachment. I checked cancellation/staging cleanup and the real SDK → ACP child process → Host path. Host remains the execution and terminal-state authority; the adapter owns request, delivery, and bounded recovery coordination. I found no justified broader rewrite or additional authority to introduce. The previously noted test-only openFile cleanup remains an optional P3 and is not a reason to hold this fix.

Validation: Runtime Host and CLI builds; 86 registry/stdio tests; 10 prompt-content tests; and the real-child-process resource-link case passed (97 distinct tests). The child-process check used Node 24 after correcting the temporary checkout's dependency resolution. Exact-head CI and all non-skipped CLI packaging/platform checks are green; the PR is mergeable. I did not rerun the full workspace suites or Zed UI smoke.

AI assistance: Codex coordinated a Reviewer Sol lifecycle pass and independently cross-checked the changed production paths and focused validation. Approval is submitted under the maintainer's standing instruction to approve reviews with no remaining blocking findings.

Preserve the layered CLI help introduced on main while keeping the ACP live-session capability summary accurate. Regenerate the combined Windows test inventory after the merge.

Generated-by: Codex
@me2seeks
me2seeks merged commit 6ce9c72 into apache:main Sep 13, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XXL Over 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants